/*
 * Copyright 2007 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package za.co.pbt.springshark;

import org.enhydra.dods.jts.LocalContextFactory;
import org.enhydra.shark.Shark;
import org.enhydra.shark.api.client.wfservice.SharkInterface;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.jta.JtaTransactionManager;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;

import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.transaction.UserTransaction;
import java.io.IOException;
import java.util.Properties;

/**
 * This class provides an entry point to the Enhydra Engine.  It automatically configures Enhydra Shark
 * to use the POJO client API.  All properties are configured using the <code>clientConfiguration</code>
 * property.  If used outside of a container, then the file "sharkdb.properties" must be present in the
 * root of the classpath.
 * <p/>
 * The following properties are allowed
 * <ul>
 * <li><code>sharkConfigurationFileLocation (String)</code>: The location of the Enhydra Shark configuration file.</li>
 * <li><code>xaUserTransactionLookupName (String)</code>: The JNDI lookup name to get the UserTransaction object.</li>
 * <li>
 * <code>standalone (boolean)</code>: The client application is used outside of a container, i.e. it is a standalone application.  If the
 * application is used in standalone mode, then a local JNDI Context is created from the file
 * <code><code>sharkdb.properties</code>.  This file is expected to be in the root of the classpath.
 * </li>
 * </ul>
 *
 * @since 1.0
 *        <p/>
 *        TODO unit test 'configureSharkOnDemand' feature
 */
public class EnhydraSharkAccessor implements InitializingBean, ResourceLoaderAware {
  private PathMatchingResourcePatternResolver resourcePatternResolver;

  /**
   * Property key that is used to specify the location of the Enhydra Shark configuration file
   */
  public final static String PROP_SHARK_CONFIGURATION_FILE_LOCATION = "sharkConfigurationFileLocation";

  /**
   * Property key that is used to specify the JNDI lookup name to get the UserTransaction object
   */
  public static final String PROP_USER_TRANSACTION_LOOKUP_NAME = "xaUserTransactionLookupName";

  /**
   * Property key that is used to specify that the client application is used outside of a container, i.e. it
   * is a standalone application.  If the application is used in standalone mode, then a local JNDI Context
   * is created from the file <code>sharkdb.properties</code>.  This file is expected to be in the root of
   * the classpath.
   * <p/>
   */
  public static final String PROP_STANDALONE = "standalone";

  /**
   * The transaction template that is used to wrap Enhydra Shark operations.
   * This template is created from the supplied transaction manager.  If used outside
   * a container, then a JtaTransactionManager is created using the transaction object
   * obtained from the locally created JNDI context.
   *
   * @see EnhydraSharkAccessor#PROP_STANDALONE
   * @see EnhydraSharkAccessor#getTransactionManager()
   */
  protected TransactionTemplate transactionTemplate;

  private Properties sharkConfiguration;
  private SharkInterface sharkInterface;
  private PlatformTransactionManager transactionManager;
  private boolean configureSharkOnDemand = false;

  /**
   * If <code>true</code>, then the configuration of Shark will occur only when an instance of
   * the shark engine is requested using the <code>getSharkInterface()</code> method.
   * If <code>false</code>, the shark engine is configured after all properties have been set.
   * Specify <code>true</code> to speed up startup of your application.
   *
   * @param configureSharkOnDemand true if Shark must be configured when needed.
   */
  public void setConfigureSharkOnDemand(boolean configureSharkOnDemand) {
    this.configureSharkOnDemand = configureSharkOnDemand;
  }

  /**
   * @return Returns the set of properties that is used to configure this bean
   */
  public Properties getSharkConfiguration() {
    return sharkConfiguration;
  }

  /**
   * @param sharkConfiguration the set of properties that is used to configure this bean
   */
  public void setSharkConfiguration(Properties sharkConfiguration) {
    this.sharkConfiguration = sharkConfiguration;
  }

  /**
   * @return Returns the transaction manager that Enhydra Shark will use
   */
  public PlatformTransactionManager getTransactionManager() {
    return transactionManager;
  }

  /**
   * @param transactionManager the transaction manager that Enhydra Shark will use
   */
  public void setTransactionManager(PlatformTransactionManager transactionManager) {
    this.transactionManager = transactionManager;
  }

  /**
   * @return Returns the SharkInterface, configuring it if required.
   */
  public SharkInterface getSharkInterface() {
    // we already have an instance of shark
    if (sharkInterface != null) {
      return sharkInterface;
    }

    // we configured shark previously but somehow lost the reference to the engine
    if (Shark.isConfigured()) {
      sharkInterface = Shark.getInstance();
      return sharkInterface;
    }

    // otherwise we need to configure shark
    return configureAndReturnInstance();
  }

  // configure shark and return an instance of the engine
  private SharkInterface configureAndReturnInstance() {
    // if working outside a container, then we must create a local Context with everything we need
    if (isStandalone()) {
      createLocalContext();
      createJtaTransactionManager();
    }

    // configure shark
    transactionTemplate.execute(new TransactionCallbackWithoutResult() {
      protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
        String configurationFile = getAbsoluteFilePathForSharkConfiguration(getConfigurationFileLocation());
        Shark.configure(configurationFile);
        sharkInterface = Shark.getInstance();
      }
    });
    return sharkInterface;
  }

  // create a JTA transaction manager
  private void createJtaTransactionManager() {
    try {
      UserTransaction userTransaction = getUserTransaction();
      transactionManager = new JtaTransactionManager(userTransaction);
      transactionTemplate = new TransactionTemplate(transactionManager);
    } catch (NamingException e) {
      // TODO Introduce project-specific exceptions
      throw new RuntimeException("Could not obtain user transaction from local context", e);
    }
  }

  // retrieve the absolute location of the file from the resource in the configuration.
  private String getAbsoluteFilePathForSharkConfiguration(String sharkConfigurationLocationPattern) {
    try {
      Resource[] resources = resourcePatternResolver.getResources(sharkConfigurationLocationPattern);
      if (resources.length != 1) {
        // TODO Introduce project-specific exceptions
        throw new RuntimeException("Multiple resources detected. Cannot resolve correct resource");
      }
      return resources[0].getFile().getPath();
    } catch (IOException e) {
      // TODO Introduce project-specific exceptions
      throw new RuntimeException("Invalid resource specified", e);
    }
  }

  // convenience method to get the <code>UserTransaction</code> object
  private UserTransaction getUserTransaction() throws NamingException {
    return (UserTransaction) new InitialContext().lookup(sharkConfiguration.getProperty("xaUserTransactionLookupName"));
  }

  // convenience method to get the status of in-container or standalone usage
  private Boolean isStandalone() {
    return Boolean.valueOf(sharkConfiguration.getProperty(PROP_STANDALONE));
  }

  // convenience method  to get the configuration file location
  private String getConfigurationFileLocation() {
    return sharkConfiguration.getProperty(PROP_SHARK_CONFIGURATION_FILE_LOCATION);
  }

  // create a local context for standalone use
  private void createLocalContext() {
    try {
      LocalContextFactory.setup("sharkdb");
    } catch (NamingException e) {
      // TODO Introduce project-specific exceptions
      throw new RuntimeException("Could not create local Context for standalone operation", e);
    }
  }

  /**
   * @see org.springframework.context.ResourceLoaderAware#setResourceLoader(org.springframework.core.io.ResourceLoader)
   * @see org.springframework.core.io.support.PathMatchingResourcePatternResolver
   */
  public void setResourceLoader(ResourceLoader resourceLoader) {
    resourcePatternResolver = new PathMatchingResourcePatternResolver(resourceLoader);
  }

  /**
   * Verify that this bean was correctly configured and apply sensible defaults.
   *
   * @throws Exception
   */
  public void afterPropertiesSet() throws Exception {
    // no Enhydra Shark configuration file specified
    if (sharkConfiguration == null) {
      throw new IllegalArgumentException("A set of properties in 'clientConfiguration' is required");
    }

    // default for the shark configuration file to "Shark.conf"
    if (!sharkConfiguration.containsKey(PROP_SHARK_CONFIGURATION_FILE_LOCATION)) {
      sharkConfiguration.put(PROP_SHARK_CONFIGURATION_FILE_LOCATION, "/Shark.conf");
    }

    // default for the shark user transaction JNDI lookup name
    if (!sharkConfiguration.containsKey(PROP_USER_TRANSACTION_LOOKUP_NAME)) {
      sharkConfiguration.put(PROP_USER_TRANSACTION_LOOKUP_NAME, "java:comp/UserTransaction");
    }

    // default to use a local JNDI context so that it works standalone
    if (!sharkConfiguration.containsKey(PROP_STANDALONE)) {
      sharkConfiguration.put(PROP_STANDALONE, true);
    }

    // create the transaction template using the specified transaction manager
    if (transactionManager != null) {
      transactionTemplate = new TransactionTemplate(transactionManager);
    }

    // configure the Enhydra Shark Engine immediately
    if (!configureSharkOnDemand) {
      sharkInterface = configureAndReturnInstance();
    }
  }
}